/*
 * SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
 * SPDX-License-Identifier: GPL-3.0-only
 */

package com.sanji.audio_test

import android.annotation.SuppressLint
import android.content.Context
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.os.ParcelFileDescriptor
import android.system.Os
import android.telecom.Call
import android.util.Log
import androidx.core.net.toFile
import androidx.documentfile.provider.DocumentFile
import com.sanji.audio_test.extension.deleteIfEmptyDir
import com.sanji.audio_test.extension.frameSizeInBytesCompat
import com.sanji.audio_test.extension.listFilesWithPathsRecursively
import com.sanji.audio_test.extension.phoneNumber
import com.sanji.audio_test.extension.toDocumentFile
import com.sanji.audio_test.format.Encoder
import com.sanji.audio_test.format.Format
import com.sanji.audio_test.output.CallMetadata
import com.sanji.audio_test.output.CallMetadataCollector
import com.sanji.audio_test.output.CallMetadataJson
import com.sanji.audio_test.output.DaysRetention
import com.sanji.audio_test.output.FormatJson
import com.sanji.audio_test.output.NoRetention
import com.sanji.audio_test.output.OutputDirUtils
import com.sanji.audio_test.output.OutputFile
import com.sanji.audio_test.output.OutputFilenameGenerator
import com.sanji.audio_test.output.OutputJson
import com.sanji.audio_test.output.OutputPath
import com.sanji.audio_test.output.ParameterType
import com.sanji.audio_test.output.PhoneNumber
import com.sanji.audio_test.output.RecordingJson
import com.sanji.audio_test.output.Retention
import com.sanji.audio_test.rule.RecordRule
import kotlinx.serialization.json.Json
import java.lang.Process
import java.nio.ByteBuffer
import java.time.Duration
import java.util.concurrent.atomic.AtomicReference
import android.os.Process as AndroidProcess

/**
 * 捕获通话音频并将其编码到用户选择的目录或默认目录中的输出文件中。
 *
 * @constructor 创建用于录制通话的线程。请注意，系统只有一个
 * [MediaRecorder.AudioSource.VOICE_CALL] 流。如果正在录制多个通话，则每个通话的录制音频可能不符合预期。
 * @param context 用于查询共享首选项和通过SAF访问文件。对象中保留了引用。
 * @param listener 用于发送完成通知。监听器从此线程调用，而不是主线程。
 * @param parentCall 用于确定输出文件名。对象中保留了对其及其子项的引用。
 */
class RecorderThread(
    private val context: Context,
    private val listener: OnRecordingCompletedListener,
    private val parentCall: Call,
) : Thread(RecorderThread::class.java.simpleName) {
    private val tag = "${RecorderThread::class.java.simpleName}/${id}"
    private val prefs = Preferences(context)

    /**
     * 录制线程状态枚举
     */
    enum class State {
        NOT_STARTED,    // 未开始
        RECORDING,      // 录制中
        FINALIZING,     // 最终化中
        COMPLETED,      // 已完成
    }

    // 线程状态
    @Volatile var state = State.NOT_STARTED
        private set
    @Volatile private var isCancelled = false

    /**
     * 保留状态枚举
     */
    enum class KeepState {
        KEEP,               // 保留
        DISCARD,            // 丢弃
        DISCARD_TOO_SHORT,  // 因时间太短而丢弃
    }

    /**
     * 是否保留录制文件。
     *
     * 在处理[RecordRule]时，此值最初设置为null。计算完成后，
     * 此字段设置为计算值。该值可以更改，包括从其他线程更改，
     * 以防用户想在通话中途覆盖规则。
     */
    private val _keepRecording = AtomicReference<KeepState>()
    var keepRecording: KeepState?
        get() = _keepRecording.get()
        set(value) {
            require(value != null)

            _keepRecording.set(value)
            Log.d(tag, "保留状态已更新: $value")

            listener.onRecordingStateChanged(this)
        }

    /**
     * 比较并设置保留状态
     * @param expected 期望的值
     * @param value 要设置的值
     */
    private fun keepRecordingCompareAndSet(expected: KeepState?, value: KeepState?) {
        require(value != null)

        if (_keepRecording.compareAndSet(expected, value)) {
            Log.d(tag, "保留状态已更新: $value")

            listener.onRecordingStateChanged(this)
        }
    }

    /**
     * 用户是否暂停了录制。
     *
     * 可以从其他线程安全更新。
     */
    // 暂停状态
    @Volatile var isPaused = false
        set(value) {
            field = value
            Log.d(tag, "暂停状态已更新: $value")

            listener.onRecordingStateChanged(this)
        }

    /**
     * 通话当前是否处于保持状态。
     *
     * 可以从其他线程安全更新。
     */
    @Volatile var isHolding = false
        set(value) {
            field = value
            Log.d(tag, "保持状态已更新: $value")

            listener.onRecordingStateChanged(this)
        }

    // 文件名相关
    private val callMetadataCollector = CallMetadataCollector(context, parentCall)
    private val outputFilenameGenerator = OutputFilenameGenerator(context)
    private val dirUtils = OutputDirUtils(context, outputFilenameGenerator.redactor)
    val outputPath: OutputPath
        get() = outputFilenameGenerator.generate(callMetadataCollector.callMetadata)

    private val minDuration: Int

    // 格式相关
    private val format: Format
    private val formatParam: UInt?
    private val sampleRate: UInt

    // 日志相关
    private lateinit var logcatPath: OutputPath
    private lateinit var logcatFile: DocumentFile
    private lateinit var logcatProcess: Process

    private var wallBeginNanos = 0L

    init {
        Log.i(tag, "为通话创建线程: $parentCall")

        minDuration = prefs.minDuration

        val savedFormat = Format.fromPreferences(prefs)
        format = savedFormat.first
        formatParam = savedFormat.second
        sampleRate = savedFormat.third ?: format.sampleRateInfo.default
    }

    /**
     * 通话详情改变时的回调
     * @param call 通话对象
     * @param details 通话详情
     */
    fun onCallDetailsChanged(call: Call, details: Call.Details) {
        callMetadataCollector.updateCallDetails(call, details)
        listener.onRecordingStateChanged(this)
    }

    /**
     * 评估录制规则
     */
    private fun evaluateRules() {
        if (keepRecording != null) {
            return
        }

        val numbers = hashSetOf<PhoneNumber>()

        if (parentCall.details.hasProperty(Call.Details.PROPERTY_CONFERENCE)) {
            for (childCall in parentCall.children) {
                childCall.details?.phoneNumber?.let { numbers.add(it) }
            }
        } else {
            parentCall.details?.phoneNumber?.let { numbers.add(it) }
        }

        Log.i(tag, "为${numbers.size}个电话号码评估录制规则")

        val rules = prefs.recordRules ?: Preferences.DEFAULT_RECORD_RULES
        val metadata = callMetadataCollector.callMetadata

        val action = try {
            RecordRule.evaluate(context, rules, numbers, metadata.direction, metadata.simSlot)
        } catch (e: Exception) {
            Log.w(tag, "评估录制规则失败", e)
            // 谨慎起见
            RecordRule.Action.SAVE
        }

        Log.i(tag, "录制规则操作: $action")

        val keep = when (action) {
            RecordRule.Action.SAVE -> true
            RecordRule.Action.DISCARD -> false
            RecordRule.Action.IGNORE -> {
                Log.i(tag, "由于录制规则而取消")
                cancel()
                return
            }
        }

        keepRecordingCompareAndSet(
            null,
            if (keep) {
                if (minDuration > 0) {
                    KeepState.DISCARD_TOO_SHORT
                } else {
                    KeepState.KEEP
                }
            } else {
                KeepState.DISCARD
            },
        )

        listener.onRecordingStateChanged(this)
    }

    /**
     * 线程运行方法
     */
    override fun run() {
        wallBeginNanos = System.nanoTime()

        var status: Status = Status.Cancelled
        var outputDocFile: DocumentFile? = null
        val additionalFiles = ArrayList<OutputFile>()

        startLogcat()

        try {
            Log.i(tag, "录制线程已启动")

            evaluateRules()

            if (isCancelled) {
                Log.i(tag, "录制在开始前已被取消")
            } else {
                state = State.RECORDING
                listener.onRecordingStateChanged(this)

                val initialPath = outputPath
                outputDocFile = dirUtils.createFileInDefaultDir(
                    initialPath.value, format.mimeTypeContainer)

                var recordingInfo: RecordingInfo? = null

                try {
                    dirUtils.openFile(outputDocFile, true).use {
                        recordingInfo = recordUntilCancelled(it)
                        Os.fsync(it.fileDescriptor)
                    }

                    status = Status.Succeeded
                } finally {
                    state = State.FINALIZING
                    listener.onRecordingStateChanged(this)

                    callMetadataCollector.update(true)
                    val finalPath = outputPath

                    if (keepRecording == KeepState.KEEP) {
                        dirUtils.tryMoveToOutputDir(
                            outputDocFile,
                            finalPath.value,
                            format.mimeTypeContainer,
                        )?.let {
                            outputDocFile = it
                        }

                        writeMetadataFile(finalPath.value, recordingInfo)?.let {
                            additionalFiles.add(it)
                        }
                    } else {
                        Log.i(tag, "删除录制文件: $finalPath")
                        outputDocFile.delete()
                        outputDocFile = null

                        status = Status.Discarded(DiscardReason.Intentional)
                    }

                    processRetention()
                }
            }
        } catch (e: Exception) {
            Log.e(tag, "录制过程中出错", e)

            if (e is PureSilenceException) {
                outputDocFile?.delete()
                outputDocFile = null

                additionalFiles.forEach {
                    it.toDocumentFile(context).delete()
                }
                additionalFiles.clear()

                val packageName = parentCall.details.accountHandle.componentName.packageName
                status = Status.Discarded(DiscardReason.Silence(packageName))
            } else {
                val mediaFrame = e.stackTrace.find { it.className.startsWith("android.media.") }
                val component = if (mediaFrame != null) {
                    FailureComponent.AndroidMedia(mediaFrame)
                } else {
                    FailureComponent.Other
                }

                status = Status.Failed(component, e)
            }
        } finally {
            Log.i(tag, "录制线程已完成")

            try {
                val logcatOutput = stopLogcat()

                // 发生错误时始终保留日志文件，以避免让用户手动启用调试模式并重现问题的麻烦
                if (prefs.isDebugMode || status is Status.Failed) {
                    additionalFiles.add(logcatOutput)
                } else {
                    Log.d(tag, "无需保留logcat")
                    logcatOutput.toDocumentFile(context).delete()
                }
            } catch (e: Exception) {
                Log.w(tag, "转储logcat失败", e)
            }

            val outputFile = outputDocFile?.let {
                OutputFile(
                    it.uri,
                    outputFilenameGenerator.redactor.redact(it.uri),
                    format.mimeTypeContainer,
                )
            }

            state = State.COMPLETED
            listener.onRecordingStateChanged(this)
            listener.onRecordingCompleted(this, outputFile, additionalFiles, status)
        }
    }

    /**
     * 取消当前录制。这将在处理下一个最小缓冲区大小后停止捕获音频，
     * 但线程不会退出，直到到目前为止编码的所有数据都已写入输出文件。
     *
     * 如果在[start]之前调用，线程将不会录制任何音频也不会创建输出文件。
     * 在这种情况下，状态将报告为[Status.Cancelled]。
     */
    fun cancel() {
        Log.d(tag, "请求取消")
        isCancelled = true
    }

    /**
     * 获取logcat文件路径
     * @return 输出路径对象
     */
    private fun getLogcatPath(): OutputPath {
        return outputPath.let {
            val path = it.value.mapIndexed { i, p ->
                p + if (i == it.value.size - 1) { ".log" } else { "" }
            }

            it.copy(value = path, redacted = it.redacted + ".log")
        }
    }

    /**
     * 启动logcat日志记录
     */
    private fun startLogcat() {
        assert(!this::logcatProcess.isInitialized) { "logcat已启动" }

        // Log.d(tag, "启动日志文件 (${BuildConfig.VERSION_NAME})")

        logcatPath = getLogcatPath()
        logcatFile = dirUtils.createFileInDefaultDir(logcatPath.value, MIME_LOGCAT)
        logcatProcess = ProcessBuilder("logcat", "*:V")
            // 这比-f更好，因为logcat实现在输出流为stdout时调用fflush()。
            // logcatFile保证具有file://方案，因为它是在默认输出目录中创建的。
            .redirectOutput(logcatFile.uri.toFile())
            .redirectErrorStream(true)
            .start()
    }

    /**
     * 停止logcat日志记录
     * @return 输出文件对象
     */
    private fun stopLogcat(): OutputFile {
        assert(this::logcatProcess.isInitialized) { "logcat未启动" }

        var uri = logcatFile.uri

        try {
            try {
                Log.d(tag, "停止日志文件")

                // 给logcat一点时间来刷新输出。它在被中断时没有任何特殊处理来刷新缓冲区。
                sleep(1000)

                logcatProcess.destroy()
            } finally {
                logcatProcess.waitFor()
            }
        } finally {
            val finalLogcatPath = getLogcatPath()
            dirUtils.tryMoveToOutputDir(logcatFile, finalLogcatPath.value, MIME_LOGCAT)?.let {
                uri = it.uri
            }
        }

        return OutputFile(uri, outputFilenameGenerator.redactor.redact(uri), MIME_LOGCAT)
    }

    /**
     * 写入元数据文件
     * @param path 文件路径
     * @param recordingInfo 录制信息
     * @return 输出文件对象，如果失败返回null
     */
    private fun writeMetadataFile(path: List<String>, recordingInfo: RecordingInfo?): OutputFile? {
        if (!prefs.writeMetadata) {
            Log.i(tag, "元数据写入已禁用")
            return null
        }

        Log.i(tag, "写入元数据文件")

        try {
            val formatJson = FormatJson(
                type = format.name,
                mimeTypeContainer = format.mimeTypeContainer,
                mimeTypeAudio = format.mimeTypeAudio,
                parameterType = ParameterType.fromParamInfo(format.paramInfo),
                parameter = formatParam ?: format.paramInfo.default,
            )
            val recordingJson = recordingInfo?.let {
                RecordingJson(
                    framesTotal = it.framesTotal,
                    framesEncoded = it.framesEncoded,
                    sampleRate = it.sampleRate,
                    channelCount = it.channelCount,
                    durationSecsWall = it.durationSecsWall,
                    durationSecsTotal = it.durationSecsTotal,
                    durationSecsEncoded = it.durationSecsEncoded,
                    bufferFrames = it.bufferFrames,
                    bufferOverruns = it.bufferOverruns,
                    wasEverPaused = it.wasEverPaused,
                    wasEverHolding = it.wasEverHolding,
                )
            }
            val outputJson = OutputJson(
                format = formatJson,
                recording = recordingJson,
            )
            val metadataJson = CallMetadataJson(
                context,
                callMetadataCollector.callMetadata,
                outputJson,
            )
            val metadataBytes = JSON_FORMAT.encodeToString(metadataJson).toByteArray()

            // 始终在默认目录中创建，然后移动，以确保我们不会与直接启动文件迁移过程发生竞争
            var metadataFile = dirUtils.createFileInDefaultDir(path, MIME_METADATA)
            dirUtils.openFile(metadataFile, true).use {
                writeFully(it.fileDescriptor, metadataBytes, 0, metadataBytes.size)
            }
            dirUtils.tryMoveToOutputDir(metadataFile, path, MIME_METADATA)?.let {
                metadataFile = it
            }

            return OutputFile(
                metadataFile.uri,
                outputFilenameGenerator.redactor.redact(metadataFile.uri),
                MIME_METADATA,
            )
        } catch (e: Exception) {
            Log.w(tag, "写入元数据文件失败", e)
            return null
        }
    }

    /**
     * 删除超过指定保留期的文件。
     *
     * "当前时间"是[CallMetadata.timestamp]，而不是实际的当前时间。
     * 过去录制的文件时间戳基于文件名，而不是文件修改时间。
     * 命名不正确的文件将被忽略。
     */
    private fun processRetention() {
        val directory = prefs.outputDirOrDefault.toDocumentFile(context)

        val retention = when (val r = Retention.fromPreferences(prefs)) {
            NoRetention -> {
                Log.i(tag, "保留所有现有文件")
                return
            }
            is DaysRetention -> r.toDuration()
        }
        Log.i(tag, "保留期为 $retention")

        val potentiallyEmptyDirs = mutableListOf<Pair<DocumentFile, List<String>>>()

        for ((item, itemPath) in directory.listFilesWithPathsRecursively()) {
            if (item.isDirectory) {
                potentiallyEmptyDirs.add(Pair(item, itemPath))
                continue
            }

            val redacted = OutputFilenameGenerator.redactTruncate(itemPath.joinToString("/"))

            val timestamp = outputFilenameGenerator.parseTimestampFromPath(itemPath)
            if (timestamp == null) {
                Log.w(tag, "忽略无法识别的路径: $redacted")
                continue
            }

            val diff = Duration.between(timestamp, callMetadataCollector.callMetadata.timestamp)

            if (diff > retention) {
                Log.i(tag, "删除 $redacted ($timestamp)")
                if (!item.delete()) {
                    Log.w(tag, "删除失败: $redacted")
                }
            }
        }

        for ((dir, dirPath) in potentiallyEmptyDirs.asReversed()) {
            if (dir.deleteIfEmptyDir()) {
                val redacted = OutputFilenameGenerator.redactTruncate(dirPath.joinToString("/"))
                Log.i(tag, "删除空目录: $redacted")
            }
        }
    }

    /**
     * 从[MediaRecorder.AudioSource.VOICE_CALL]录制直到调用[cancel]或发生音频捕获或编码错误。
     *
     * [pfd]不会被此方法关闭。
     * 
     * @param pfd 文件描述符
     * @return 录制信息
     */
    @SuppressLint("MissingPermission")
    private fun recordUntilCancelled(pfd: ParcelFileDescriptor): RecordingInfo {
        AndroidProcess.setThreadPriority(AndroidProcess.THREAD_PRIORITY_URGENT_AUDIO)

        val minBufSize = AudioRecord.getMinBufferSize(sampleRate.toInt(), CHANNEL_CONFIG, ENCODING)
        if (minBufSize < 0) {
            throw Exception("查询最小缓冲区大小时失败: $minBufSize")
        }
        Log.d(tag, "AudioRecord最小缓冲区大小: $minBufSize")

        val audioRecord = AudioRecord(
            MediaRecorder.AudioSource.VOICE_CALL,
            sampleRate.toInt(),
            CHANNEL_CONFIG,
            ENCODING,
            // 在某些设备上，MediaCodec偶尔会有处理时间的突然激增，
            // 因此使用较大的内部缓冲区以减少录制端溢出的机会。
            minBufSize * 6,
        )
        val initialBufSize = audioRecord.bufferSizeInFrames *
                audioRecord.format.frameSizeInBytesCompat
        Log.d(tag, "AudioRecord初始缓冲区大小: $initialBufSize")

        Log.d(tag, "AudioRecord格式: ${audioRecord.format}")

        // 我的RAII在哪里？ :(
        try {
            audioRecord.startRecording()

            try {
                val container = format.getContainer(pfd.fileDescriptor)

                try {
                    // audioRecord.format具有检测到的本机采样率
                    val mediaFormat = format.getMediaFormat(audioRecord.format, formatParam)
                    val encoder = format.getEncoder(mediaFormat, container)

                    try {
                        encoder.start()

                        try {
                            return encodeLoop(audioRecord, encoder, minBufSize)
                        } finally {
                            encoder.stop()
                        }
                    } finally {
                        encoder.release()
                    }
                } finally {
                    container.release()
                }
            } finally {
                audioRecord.stop()
            }
        } finally {
            audioRecord.release()
        }
    }

    /**
     * 将捕获的原始音频编码到输出文件的主循环。
     *
     * 循环会一直运行直到调用[cancel]。此时，将不再从[audioRecord]读取数据，
     * 并将[encoder]的剩余输出数据写入输出文件。
     * 如果[audioRecord]无法捕获数据，循环将表现得像调用了[cancel]一样
     * (即中止，但确保输出文件有效)。
     *
     * 取消从音频源读取的大概时间是处理最小缓冲区大小所需的时间。
     * 此外，还需要额外的时间将剩余的编码数据写入输出文件。
     *
     * @param audioRecord 必须已调用[AudioRecord.startRecording]
     * @param encoder 必须已调用[Encoder.start]
     * @param bufSize 每次[AudioRecord.read]操作的最小缓冲区大小
     *
     * @throws Exception 如果音频录制器或编码器遇到错误
     */
    private fun encodeLoop(
        audioRecord: AudioRecord,
        encoder: Encoder,
        bufSize: Int,
    ): RecordingInfo {
        var numFramesTotal = 0L
        var numFramesEncoded = 0L
        var bufferOverruns = 0
        var wasEverPaused = false
        var wasEverHolding = false
        var wasReadSamplesError = false
        var wasPureSilence = true
        val frameSize = audioRecord.format.frameSizeInBytesCompat

        // 使用稍大的缓冲区以减少负载下的问题机会
        val factor = 2
        val buffer = ByteBuffer.allocateDirect(bufSize * factor)
        val bufferFrames = buffer.capacity().toLong() / frameSize
        val bufferNs = bufferFrames * 1_000_000_000L /
                audioRecord.sampleRate /
                audioRecord.channelCount
        Log.d(tag, "缓冲区为 ${buffer.capacity()} 字节, $bufferFrames 帧, ${bufferNs}ns")

        while (!isCancelled) {
            val begin = System.nanoTime()
            // 我们进行非阻塞读取，因为在Samsung设备上，当通话结束时，
            // 音频设备会立即停止产生数据并永远阻塞直到下一个通话激活。
            val n = audioRecord.read(buffer, buffer.remaining(), AudioRecord.READ_NON_BLOCKING)
            val recordElapsed = System.nanoTime() - begin
            var encodeElapsed = 0L

            if (n < 0) {
                Log.e(tag, "从 $audioRecord 读取样本时出错: $n")
                isCancelled = true
                wasReadSamplesError = true
            } else if (n == 0) {
                // 等待相当于最小缓冲区大小的墙上时钟时间
                sleep(bufferNs / 1_000_000L / factor)
                continue
            } else {
                buffer.limit(n)

                if (wasPureSilence) {
                    for (i in 0 until n / 2) {
                        if (buffer.getShort(2 * i) != 0.toShort()) {
                            wasPureSilence = false
                            break
                        }
                    }
                }

                val encodeBegin = System.nanoTime()

                // 如果被用户暂停或保持，继续录制，但丢弃数据
                if (!isPaused && !isHolding) {
                    encoder.encode(buffer, false)
                    numFramesEncoded += n / frameSize
                } else {
                    wasEverPaused = wasEverPaused || isPaused
                    wasEverHolding = wasEverHolding || isHolding
                }

                numFramesTotal += n / frameSize

                encodeElapsed = System.nanoTime() - encodeBegin

                buffer.clear()
            }

            val totalElapsed = System.nanoTime() - begin
            if (encodeElapsed > bufferNs) {
                bufferOverruns += 1
                Log.w(tag, "${encoder.javaClass.simpleName} 耗时过长: " +
                        "总时间戳=${numFramesTotal.toDouble() / audioRecord.sampleRate /
                                audioRecord.channelCount}s, " +
                        "编码时间戳=${numFramesEncoded.toDouble() / audioRecord.sampleRate /
                                audioRecord.channelCount}s, " +
                        "缓冲区=${bufferNs / 1_000_000.0}ms, " +
                        "总计=${totalElapsed / 1_000_000.0}ms, " +
                        "录制=${recordElapsed / 1_000_000.0}ms, " +
                        "编码=${encodeElapsed / 1_000_000.0}ms")
            }

            val secondsEncoded = numFramesEncoded / audioRecord.sampleRate / audioRecord.channelCount
            if (secondsEncoded >= minDuration) {
                keepRecordingCompareAndSet(KeepState.DISCARD_TOO_SHORT, KeepState.KEEP)
            }
        }

        if (wasReadSamplesError) {
            throw ReadSamplesException()
        } else if (wasPureSilence) {
            throw PureSilenceException()
        }

        // 使用空缓冲区发送EOF
        Log.d(tag, "向编码器发送EOF")
        buffer.limit(buffer.position())
        encoder.encode(buffer, true)

        val recordingInfo = RecordingInfo(
            System.nanoTime() - wallBeginNanos,
            numFramesTotal,
            numFramesEncoded,
            audioRecord.sampleRate,
            audioRecord.channelCount,
            bufferFrames,
            bufferOverruns,
            wasEverPaused,
            wasEverHolding,
        )

        Log.d(tag, "输入完成: $recordingInfo")

        return recordingInfo
    }

    companion object {
        // 音频配置常量
        private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
        private const val ENCODING = AudioFormat.ENCODING_PCM_16BIT

        // MIME类型常量
        const val MIME_LOGCAT = "text/plain"
        const val MIME_METADATA = "application/json"

        // JSON格式化配置
        private val JSON_FORMAT = Json {
            prettyPrint = true
        }
    }

    /**
     * 录制信息数据类
     * @param wallDurationNanos 墙上时钟持续时间(纳秒)
     * @param framesTotal 总帧数
     * @param framesEncoded 已编码帧数
     * @param sampleRate 采样率
     * @param channelCount 声道数
     * @param bufferFrames 缓冲区帧数
     * @param bufferOverruns 缓冲区溢出次数
     * @param wasEverPaused 是否曾被暂停
     * @param wasEverHolding 是否曾被保持
     */
    private data class RecordingInfo(
        val wallDurationNanos: Long,
        val framesTotal: Long,
        val framesEncoded: Long,
        val sampleRate: Int,
        val channelCount: Int,
        val bufferFrames: Long,
        val bufferOverruns: Int,
        val wasEverPaused: Boolean,
        val wasEverHolding: Boolean,
    ) {
        // 各种持续时间计算
        val durationSecsWall = wallDurationNanos.toDouble() / 1_000_000_000.0
        val durationSecsTotal = framesTotal.toDouble() / sampleRate / channelCount
        val durationSecsEncoded = framesEncoded.toDouble() / sampleRate / channelCount

        override fun toString() = buildString {
            append("墙上时钟: ${"%.1f".format(durationSecsWall)}s")
            append(", 总计: $framesTotal 帧 (${"%.1f".format(durationSecsTotal)}s)")
            append(", 已编码: $framesEncoded 帧 (${"%.1f".format(durationSecsEncoded)}s)")
            append(", 采样率: $sampleRate")
            append(", 声道数: $channelCount")
            append(", 缓冲区帧数: $bufferFrames")
            append(", 缓冲区溢出: $bufferOverruns")
            append(", 曾被暂停: $wasEverPaused")
            append(", 曾被保持: $wasEverHolding")
        }
    }

    /**
     * 读取样本异常类
     * @param cause 异常原因
     */
    private class ReadSamplesException(cause: Throwable? = null)
        : Exception("读取音频样本失败", cause)

    /**
     * 纯静音异常类
     * @param cause 异常原因
     */
    private class PureSilenceException(cause: Throwable? = null)
        : Exception("音频包含纯静音", cause)

    /**
     * 失败组件密封接口
     */
    sealed interface FailureComponent {
        /**
         * Android媒体组件失败
         * @param stackFrame 堆栈帧信息
         */
        data class AndroidMedia(val stackFrame: StackTraceElement) : FailureComponent

        /** 其他组件失败 */
        data object Other : FailureComponent
    }

    /**
     * 丢弃原因密封接口
     */
    sealed interface DiscardReason {
        /** 故意丢弃 */
        data object Intentional : DiscardReason

        /**
         * 静音丢弃
         * @param callPackage 通话包名
         */
        data class Silence(val callPackage: String) : DiscardReason
    }

    /**
     * 状态密封接口
     */
    sealed interface Status {
        /** 成功 */
        data object Succeeded : Status

        /**
         * 失败
         * @param component 失败组件
         * @param exception 异常
         */
        data class Failed(val component: FailureComponent, val exception: Exception?) : Status

        /**
         * 已丢弃
         * @param reason 丢弃原因
         */
        data class Discarded(val reason: DiscardReason) : Status

        /** 已取消 */
        data object Cancelled : Status
    }

    /**
     * 录制完成监听器接口
     */
    interface OnRecordingCompletedListener {
        /**
         * 当暂停状态、保留状态或输出文件名改变时调用。
         * @param thread 录制线程
         */
        fun onRecordingStateChanged(thread: RecorderThread)

        /**
         * 当录制完成时调用，无论成功与否。[file]是输出文件。
         *
         * [file]在几种情况下可能为null:
         * * 通话匹配默认丢弃录制的录制规则
         * * 用户通过通知故意选择丢弃录制
         * * 无法创建输出文件
         * * 线程在开始前被取消
         *
         * 请注意，对于大多数错误，输出文件*不会*被删除。
         *
         * [additionalFiles]是与主输出文件关联的附加文件，
         * 如果用户通过通知选择这样做，应与主文件一起删除。
         * 即使[file]为null，这些文件也可能存在(例如日志文件)。
         * 
         * @param thread 录制线程
         * @param file 输出文件
         * @param additionalFiles 附加文件列表
         * @param status 状态
         */
        fun onRecordingCompleted(
            thread: RecorderThread,
            file: OutputFile?,
            additionalFiles: List<OutputFile>,
            status: Status,
        )
    }
}